게시판 만들기

✒️ 2025-05-28 11:46 내용 수정


실습 목표


실습 흐름

  1. DB와 Spring을 Mybatis로 연결한다.
  2. DB에 테이블을 추가한다.
  3. DTO와 DAO를 만들고, Context_3_dao에 DAO Bean을 추가한다.
  4. 조회, 수정, 삭제 기능을 DAO에 추가한다.
  5. DAO에 추가한 내용의 query문을 mapper에도 추가한다.
  6. JSP 페이지를 만들어 각 기능에 맞는 데이터를 보내거나 받을 수 있도록 작성한다.
  7. Controller를 생성하여 DAO를 호출하고, JSP에서 보낸 데이터를 이어준다.
  8. ServletContext에 Controller Bean을 추가한다.
  9. 게시판의 기본 기능을 구현하고 나서 로그인(아이디 중복 체크 포함), 회원가입, 로그아웃 기능을 추가한다.

흐름 내용

DB에 테이블 추가

```sql
--시퀀스
CREATE SEQUENCE SEQ_BOARD_IDX;

--테이블
CREATE TABLE BOARD(
	IDX NUMBER(3) PRIMARY KEY,  --번호
	NAME VARCHAR2(100) NOT NULL, --작성자
	SUBJECT VARCHAR2(255) NOT NULL, --게시글 이름
	CONTENT CLOB,  --게시글 내용
	PWD VARCHAR2(100),  --비밀번호
	IP VARCHAR2(100),  --IP
	REGDATE DATE,  --작성일
	READHIT NUMBER(3) DEFAULT 0, --조회수
	REF INT, --기준글번호(댓글의 메인글 번호)
	STEP INT,  --댓글순서
	DEPTH INT,  --대댓글
	DEL_INFO NUMBER(2)  --글 삭제여부
);

-- 샘플 데이터 추가
INSERT INTO BOARD VALUES(
	SEQ_BOARD_IDX.nextVal,
	'에이스',
	'게시판 첫 글',
	'게시판 첫 번째 글은 내가 작성했다',
	'1234',
	'192.0.0.2',
	SYSDATE,
	0,
	SEQ_BOARD_IDX.currval,
	0,
	0,
	0
);

-- 댓글 샘플 데이터
INSERT INTO BOARD VALUES(
	SEQ_BOARD_IDX.nextVal,
	'브라보',
	'오늘 날씨',
	'오늘 날씨 비 오고 눈 내림',
	'1234',
	'192.0.0.2',
	SYSDATE,
	0,
	1,
	1,
	1,
	0
);

-- 댓글의 댓글 샘플 데이터
INSERT INTO BOARD VALUES(
	SEQ_BOARD_IDX.nextVal,
	'칼리',
	'대댓글',
	'대대대대댓글',
	'1234',
	'192.0.0.3',
	SYSDATE,
	0,
	1,
	2,
	2,
	0
);

프로젝트 기본 설정

board spring 1.png

DB에 연결

  1. Context_1_dataSource
package context;

import javax.sql.DataSource;

import org.apache.commons.dbcp.BasicDataSource;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class Context_1_dataSource {
	
	@Bean
	public DataSource ds() {
		BasicDataSource ds = new BasicDataSource();
		ds.setDriverClassName("oracle.jdbc.OracleDriver");
		ds.setUrl("jdbc:oracle:thin:@localhost:1521:xe");
		ds.setUsername("계정명");
		ds.setPassword("비밀번호");
		return ds;
	}
}
  1. Context_2_myBatis
package context;

import javax.sql.DataSource;

import org.apache.ibatis.session.SqlSessionFactory;
import org.mybatis.spring.SqlSessionFactoryBean;
import org.mybatis.spring.SqlSessionTemplate;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.ClassPathResource;

import lombok.RequiredArgsConstructor;

@Configuration
@RequiredArgsConstructor
public class Context_2_myBatis {
	
	final DataSource ds;
	
	@Bean
	public SqlSessionFactory factoryBean() throws Exception{
		SqlSessionFactoryBean factoryBean = new SqlSessionFactoryBean();
		
		factoryBean.setDataSource(ds);
		
		// mapper를 알고있는 mybatis-config.xml 파일의 위치를 알려줘야 함
		factoryBean.setConfigLocation(new ClassPathResource("config/mybatis/mybatis-config.xml"));
		
		return factoryBean.getObject();
	}

	@Bean
	public SqlSessionTemplate sqlSessionBean() throws Exception {
		return new SqlSessionTemplate(factoryBean());
	}
}
  1. mybatis-config.xml
    • 게시판의 정보와 회원 정보를 가져오기 위해 mapper를 2개 사용해야 한다.
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0//EN" "HTTP://mybatis.org/dtd/mybatis-3-config.dtd">

<configuration>
	<settings>
		<setting name="cacheEnabled" value="false" />
		<setting name="useGeneratedKeys" value="true" />
		<setting name="defaultExecutorType" value="REUSE" />
	</settings>
	
	<typeAliases>
		<typeAlias type="dto.BoardDTO" alias="board"/>
		<typeAlias type="dto.MemberDTO" alias="member"/>
	</typeAliases>

	<mappers>
		<mapper resource="config/mybatis/mapper/board.xml" />
		<mapper resource="config/mybatis/mapper/member.xml" />
	</mappers>
</configuration>
  1. board_mapper.xml
    • 게시글 조회, 게시글 개수 조회, 게시글 상제 조회, 조회수 증가, 게시글 추가, 글 제거(처럼 보이도록 수정), 댓글 계층 구조 변경, 댓글 추가 기능을 추가한다.
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="b">
	<!-- 페이지별 게시글 조회 -->
	<select id="board_list" parameterType="java.util.HashMap" resultType="board">
		SELECT * FROM (SELECT RANK() OVER(ORDER BY REF DESC, STEP) no, b.* FROM BOARD b)
		WHERE no BETWEEN #{start} AND #{end}
	</select>
	
	<!-- 전체 게시물 수 조회 -->
	<select id="board_count" resultType="int">
		SELECT COUNT(*) FROM BOARD
	</select>
	
	<!-- 게시글 상세보기 -->
	<select id="board_one" parameterType="int" resultType="board">
		SELECT * FROM BOARD WHERE IDX = #{idx}
	</select>
	
	<!-- 조회수 증가 -->
	<update id="board_update_readhit" parameterType="int">
		UPDATE BOARD
		SET READHIT = READHIT + 1
		WHERE IDX = #{idx}
	</update>
	
	<!-- 게시글 등록 -->
	<insert id="board_insert" parameterType="board">
		INSERT INTO BOARD VALUES(
			SEQ_BOARD_IDX.nextVal,
			#{name},
			#{subject},
			#{content},
			#{pwd},
			#{ip},
			sysdate,
			0,
			SEQ_BOARD_IDX.currVal,
			0,
			0,
			0
		)
	</insert>
	
	<!-- 글 삭제하기 -->
	<update id="board_delete" parameterType="board">
		UPDATE BOARD
		SET SUBJECT = #{subject}, NAME = #{name}, DEL_INFO = -1
		WHERE IDX=#{idx}
	</update>
	
	<!-- 댓글 등록을 위한 step + 1 -->
	<update id="board_update_step" parameterType="board">
		UPDATE BOARD
		SET STEP = STEP + 1
		WHERE REF = #{ref} AND STEP > #{step} 
	</update>
	
	<!-- 댓글 등록 -->
	<insert id="board_reply" parameterType="board">
		INSERT INTO BOARD VALUES(
			SEQ_BOARD_IDX.nextVal,
			#{name},
			#{subject},
			#{content},
			#{pwd},
			#{ip},
			sysdate,
			0,
			#{ref},
			#{step},
			#{depth},
			0
		)
	</insert>
</mapper>
  1. member_mapper.xml
    • 회원 정보를 체크하는 로그인 체크, 회원을 새로 추가하는 회원 가입 기능을 추가한다.
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="m">
	<!-- 로그인 체크 -->
	<select id="login_check" parameterType="String" resultType="member">
		SELECT * FROM MEMBER WHERE ID = #{id}
	</select>
	
	<!-- 회원가입 -->
	<insert id="member_insert" parameterType="member">
		INSERT INTO MEMBER VALUES(
			SEQ_MEMBER_IDX.nextVal,
			#{name},
			#{id},
			#{pwd},
			#{email}
		)
	</insert>
</mapper>

Ajax

var xhr = null;

function createRequest() {
	if (xhr != null) {
		return;
	}
	if (window.ActiveXObject) {
		xhr = new ActiveXObject("Microsoft.XMLHTTP"); // IE 환경
	} else {
		xhr = new XMLHttpRequest(); // 기타 브라우저 환경
	}
}

function sendRequest(url, param, callback, method) {

	// HttpRequest 생성
	createRequest();

	// 전송 타입 구분
	var httpMethod = (method != 'POST' && method != 'post') ? 'GET' : 'POST';

	// 파라미터 구분
	var httpParam = (param == null || param == '') ? null : param;

	// 접근 url
	var httpURL = url;

	// 요청 방식이 GET이고 전달할 파라미터가 있다면 새 url 경로 제작
	if (httpMethod == 'GET' && httpParam != null) {
		httpURL = httpURL+'?'+httParam;
	}

	// 서버로 보낼 Ajax 요청 형식
	xhr.open(httpMethod, httpURL, true);

	// requestHeader 설정 : Content-Type 지정
	xhr.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");

	// 작업이 완료된 후 호출할 callback 메소드 지정
	xhr.onreadystatechange = callback;

	// Ajax 요청을 서버로 전달
	xhr.send(httpMethod == 'POST' ? httpParam : null);
}

DTO와 DAO

  1. BoardDTO : 게시판 정보를 저장하는 DTO
    • lombok 라이브러리의 @Data 사용
package dto;

import lombok.Data;

@Data
public class BoardDTO {
	private int idx;
	private int readhit;
	private int ref;
	private int step;
	private int depth;
	private int del_info;
	
	private String name;
	private String subject;
	private String content;
	private String pwd;
	private String ip;
	private String regdate;
}
  1. MemberDTO : 회원 정보를 저장하는 DTO
    • lombok 라이브러리의 @Getter와 @Setter 사용
package dto;

import lombok.Getter;
import lombok.Setter;

@Setter
@Getter
public class MemberDTO {
	private int idx;
	private String name;
	private String id;
	private String pwd;
	private String email;
}
  1. BoardDAO
    • SqlSession을 통해 DB와 연결해서 게시판와 연관된 기능을 수행할 메소드들을 정리
package dao;

import java.util.HashMap;
import java.util.List;

import org.apache.ibatis.session.SqlSession;

import dto.BoardDTO;
import lombok.RequiredArgsConstructor;

@RequiredArgsConstructor
public class BoardDAO {
	
	final SqlSession sqlSession;
	
	// 페이지별 게시글 조회
	public List<BoardDTO> selectList(HashMap<String, Integer> map) {
		return sqlSession.selectList("b.board_list", map);
	}
	
	// 전체 게시물 수 조회
	public int getRowTotal() {
		return sqlSession.selectOne("b.board_count");
	}
	
	// 게시글 하나 상세보기
	public BoardDTO selectOne(int idx) {
		return sqlSession.selectOne("b.board_one", idx);
	}
	
	// 조회수 증가
	public int update_readhit(int idx) {
		return sqlSession.update("b.board_update_readhit", idx);
	}
	
	// 새 글 등록
	public int insert(BoardDTO dto) {
		return sqlSession.insert("b.board_insert", dto);
	}
	
	// 글 삭제
	public int delete_update(BoardDTO dto) {		
		return sqlSession.update("b.board_delete", dto);
	}
	
	// 댓글 추가를 위한 step + 1
	public int update_step(BoardDTO dto) {
		return sqlSession.update("b.board_update_step", dto);
	}
	
	// 댓글 등록
	public int reply(BoardDTO dto) {
		return sqlSession.insert("b.board_reply", dto);
	}
}
  1. MemberDAO
    • SqlSession을 통해 DB와 연결해서 회원정보 및 로그인과 연관된 기능을 수행할 메소드들을 정리
package dao;

import org.apache.ibatis.session.SqlSession;

import dto.MemberDTO;
import lombok.RequiredArgsConstructor;

@RequiredArgsConstructor
public class MemberDAO {
	final SqlSession sqlSession;
	
	// 로그인 체크
	public MemberDTO selectOne(String id) {
		return sqlSession.selectOne("m.login_check", id);
	}
	
	// 회원가입
	public int insert(MemberDTO dto) {
		return sqlSession.insert("m.member_insert", dto);
	}
}
  1. Context_3_dao
    • BoardDAO와 MemberDAO Bean 객체를 등록
package context;

import org.apache.ibatis.session.SqlSession;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import dao.BoardDAO;
import dao.MemberDAO;
import dao.MemberDAO;
import dao.MemberDAO;
import dao.MemberDAO;

@Configuration
public class Context_3_dao {
	
	@Bean
	public BoardDAO boardDAO(SqlSession sqlSession) {
		return new BoardDAO(sqlSession);
	}
	
	@Bean
	public MemberDAO memberDAO(SqlSession sqlSession) {
		return new MemberDAO(sqlSession);
	}
}

Controller

  1. BoardController
    • BoardDAO와 MemberDAO를 생성자 주입하고, HttpSession 객체는 필드 주입한다.
    • 게시판 전체 기능과 회원가입 및 로그인 기능들을 담당한다.
package com.nogroup.board;

import java.util.HashMap;
import java.util.List;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpSession;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;

import dao.BoardDAO;
import dao.MemberDAO;
import dto.BoardDTO;
import dto.MemberDTO;
import lombok.RequiredArgsConstructor;
import util.Common;
import util.Paging;

@Controller
@RequiredArgsConstructor
public class BoardController {

	final BoardDAO board_dao;
	final MemberDAO member_dao;
	
	@Autowired
	HttpServletRequest request;
	
	@Autowired
	HttpSession session;
	
	// 게시글 전체를 페이지별로
	@RequestMapping(value= {"/", "board_list"})
	public String list(Model model, @RequestParam(required=false, defaultValue="1") int page) {
		
		int start = (page - 1) * Common.Board.BLOCKLIST + 1;
		int end = start + Common.Board.BLOCKLIST - 1;
		
		HashMap<String, Integer> map = new HashMap<String, Integer>();
		map.put("start", start);
		map.put("end", end);
	
		// 페이지 번호에 따른 전체 게시글 조회
		List<BoardDTO> list = board_dao.selectList(map);
		
		// 전체 게시글 수 조회
		int rowTotal = board_dao.getRowTotal();
		
		// 페이지 메뉴 생성하기
		String pageMenu = Paging.getPaging("board_list", 
											page, 
											rowTotal, 
											Common.Board.BLOCKLIST, 
											Common.Board.BLOCKPAGE);
		
		request.getSession().removeAttribute("show");
		
		model.addAttribute("list", list);
		model.addAttribute("pageMenu", pageMenu);
		
		return Common.Board.VIEW_PATH+"board_list.jsp?page="+page;
	}
	
	// 게시글 상세보기
	@RequestMapping("view")
	public String view(Model model, int idx, int page) {
		
		BoardDTO dto = board_dao.selectOne(idx);
		
		// 조회수 증가
		HttpSession session = request.getSession();
		String show = (String)session.getAttribute("show");
		
		if (show == null) {
			int res = board_dao.update_readhit(idx);
			session.setAttribute("show", "0");
		}
		
		model.addAttribute("dto", dto);
		return Common.Board.VIEW_PATH+"board_view.jsp?page="+page;
	}
	
	// 글 등록
	@RequestMapping("insert_form")
	public String insert_form(int page) {
		MemberDTO show = (MemberDTO)session.getAttribute("id");
		
		if (show == null) {
			return Common.Board.VIEW_PATH + "login_form.jsp";
		}
		
		return Common.Board.VIEW_PATH+"insert_form.jsp?page="+page+"&name="+show.getName();
	}
	
	@RequestMapping("insert")
	public String insert(BoardDTO dto, int page) {
		String ip = request.getRemoteAddr();
		dto.setIp(ip);
		
		int res = board_dao.insert(dto);
	
		if (res > 0) {
			return "redirect:board_list?page="+page;
		}
		
		return null;
	}
	
	// 글 삭제
	@RequestMapping("board_delete")
	@ResponseBody
	public String delete(int idx) {
		BoardDTO ori_dto = board_dao.selectOne(idx);
		
		ori_dto.setSubject("삭제된 글 입니다");
		ori_dto.setName("unknown");
		
		int res = board_dao.delete_update(ori_dto);
	
		if (res == 1) {
			return "[{'result':'yes'}]";
		} else {
			return "[{'result':'no'}]";
		}
	}
		
	// 댓글 추가
	@RequestMapping("reply_form")
	public String reply_form(int idx,  int page) {
		MemberDTO show = (MemberDTO)session.getAttribute("id");
		
		if(show == null) {
			return Common.Board.VIEW_PATH + "login_form.jsp";
		}
		 
		return Common.Board.VIEW_PATH + "reply_form.jsp?idx="+idx+"&page="+page;
	} 
	
	@RequestMapping("reply")
	public String reply(BoardDTO dto, int idx, int page) {
		// Controller에서 dao를 3번이나 접근하기에 이후에 service에서 reply()메소드를 생성해서
		// dao 접근 및 댓글 추가 작업을 끝내는 기능을 추가하고
		// Controller에서 service.reply()를 한 번만 호출해서 모든 기능을 수행하도록 변경할 수 있다.
		
		String ip = request.getRemoteAddr();
		
		BoardDTO ori_dto = board_dao.selectOne(idx);
		
		int res = board_dao.update_step(ori_dto);
		
		dto.setIp(ip);
		
		// 댓글이 들어갈 위치 선정
		dto.setRef(ori_dto.getRef());
		dto.setStep(ori_dto.getStep()+1);
		dto.setDepth(ori_dto.getDepth()+1);
		
		int res2 = board_dao.reply(dto);
	
		if (res2 > 0) {
			return "redirect:board_list?page="+page;
		}
		
		return null;
	}
	
	 //로그인
	@RequestMapping("login")
	@ResponseBody
	public String login(String id, String pwd) {
		MemberDTO dto = member_dao.selectOne(id);
		
		// dto가 null일 경우 id가 존재하지 않음
		if(dto == null) {
			return "[{'param':'no_id'}]";
		}
		
		// 우리가 입력받은 pwd와 DB에 저장된 비밀번호를 비교
		if(!pwd.equals(dto.getPwd())) {
			return "[{'param':'no_pwd'}]";
		}
		
		// 아이디와 비밀번호에 문제 없으므로 로그인 가능
		// 세션에 바인딩
		session.setAttribute("id", dto);
		
		// 로그인에 성공한 경우
		return "[{'param':'clear'}]";
	}
	
	// 로그인 페이지로 이동
	@RequestMapping("login_form")
	public String login_form() {
		return Common.Board.VIEW_PATH + "login_form.jsp";
	}
	
	// 로그아웃
	@RequestMapping("logout")
	public String logout() {
		session.removeAttribute("id");
		
		return "redirect:board_list";
	}
	
	// 회원가입
	@RequestMapping("member_insert_form")
	public String member_insert_form() {
		return Common.Board.VIEW_PATH + "member_insert_form.jsp";
	}
	
	// 중복체크
	@RequestMapping("check_id")
	@ResponseBody
	public String check_id(String id) {
		MemberDTO dto = member_dao.selectOne(id);
		
		if (dto == null) {
			return "[{'res':'yes'}]";
		} else {
			return "[{'res':'no'}]";
		}
	}
	
	// 회원가입 DB에 전달
	@RequestMapping("member_insert")
	@ResponseBody
	public String member_insert(MemberDTO dto) {
		int res = member_dao.insert(dto);
		
		if (res > 0) {
			return "[{'regi':'yes'}]";
		} else {
			return "[{'regi':'no'}]";
		}
	}
}
  1. ServletContext
    • BoardController Bean 객체를 등록
package mvc;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

import com.nogroup.board.BoardController;

import dao.BoardDAO;
import dao.MemberDAO;

@Configuration
@EnableWebMvc
//@ComponentScan("com.nogroup.board") // 자동 생성을 할 때 사용
public class ServletContext implements WebMvcConfigurer{
	@Override
	public void addResourceHandlers(ResourceHandlerRegistry registry) {
		registry.addResourceHandler("/resources/**").addResourceLocations("/resources/");
	}
	
	@Bean
	public BoardController boardController(BoardDAO boardDAO, MemberDAO memberDAO) {
		return new BoardController(boardDAO, memberDAO);
	}
}

util 패키지

  1. Common
    • JSP 파일의 경로를 저장할 상수와 페이징 처리에 필요한 상수를 저장한다.
package util;

public class Common {
	
	public static class Board{
		public final static String VIEW_PATH = "/WEB-INF/views/board/";
		
		// 한 페이지에 보여줄 게시물 수
		public final static int BLOCKLIST = 10;
		
		// 페이지 메뉴 수 
		public final static int BLOCKPAGE = 3;
	}
}
  1. Paging
    • 게시판의 페이징 처리를 담당하며, 게시판 만들기의 페이징 처리와 동일한 코드다.
package util;

public class Paging {
	
	public static String getPaging(String pageURL, int nowPage, int rowTotal, int blockList, int blockPage) {
		int totalPage, startPage, endPage;
		boolean isPrevPage, isNextPage;
		StringBuffer sb;
		
		isPrevPage = isNextPage = false;
		
		totalPage = (int)(rowTotal / blockList);
		
		if (rowTotal % blockList != 0) totalPage++;
		
		if (nowPage > totalPage) nowPage = totalPage;
		
		startPage = (int)(((nowPage - 1)/blockPage)*blockPage+1);
		endPage = startPage + blockPage -1;
		
		if (endPage > totalPage) endPage = totalPage;
		
		if (endPage < totalPage) isNextPage = true;
		
		if (startPage > 1) isPrevPage = true;
		
		sb = new StringBuffer();
		if(isPrevPage) {
			sb.append("<a href='"+pageURL+"?page=");
			sb.append(startPage-1);
			sb.append("'><img src='resources/img/btn_prev.gif'></a>");
		} else {
			sb.append("<img src='resources/img/btn_prev.gif'>");
		}
		
		sb.append("");
		for (int i = startPage; i <= endPage; i++) {
			if(i > totalPage) break;
			if(i == nowPage) {
				sb.append("&nbsp;<b><font color='#ff0000'>");
				sb.append(i);
				sb.append("</font></b>");
			} else {
				sb.append("&nbsp;<a href='"+pageURL+"?page=");
				sb.append(i);
				sb.append("'>");
				sb.append(i);
				sb.append("</a>");
			}
		}
		
		sb.append("&nbsp; ");
		
		if(isNextPage) {
			sb.append("<a href='"+pageURL+"?page=");
			sb.append(endPage+1);
			sb.append("'><img src='resources/img/btn_next.gif'></a>");
		} else {
			sb.append("<img src='resources/img/btn_next.gif'>");
		}
		
		return sb.toString();
	}
}

JSP

  1. board_list.jsp
    • 게시판 메인 페이지로, 모든 게시글을 조회할 수 있다.
    • 로그인을 해야만 새 글 작성, 글 삭제, 댓글 추가를 할 수 있다.
    • 회원 정보가 없다면 새로 회원가입할 수 있다.
<%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8"%>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<%@ taglib prefix="fn" uri="http://java.sun.com/jsp/jstl/functions" %>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Insert title here</title>
	<style>
		a{text-decoration:none;}
		table{border-collapse:collapse; width:700px;}
	</style>
</head>
<body>
	<table border="1" align="center">
		<tr>
			<!-- 세션에 값이 있으면 로그아웃 버튼이, 세션이 없으면 로그인, 회원가입 버튼이 보이게 설정 -->
			<td colspan="5" align="right">
				<c:choose>
					<c:when test="${empty id}">
						<input type="button" value="로그인" onclick="location.href='login_form'">
						<input type="button" value="회원가입" onclick="location.href='member_insert_form'">
					</c:when>
					<c:otherwise>
						<input type="button" value="로그아웃" onclick="location.href='logout'">
					</c:otherwise>
				</c:choose>
			</td>
			
		</tr>
		<tr>
			<td colspan="5"><img src="resources/img/title_04.gif"></td>
		</tr>
		<tr>
			<th>번호</th>
			<th>제목</th>
			<th>작성자</th>
			<th>작성일</th>
			<th>조회수</th>
		</tr>
		
		<c:forEach var="dto" items="${list}">
			<tr>
				<td align="center">${dto.idx}</td>
				<!-- 댓글일 경우 들여쓰기 -->
				<td>
					<c:forEach begin="1" end="${dto.depth}">&nbsp;</c:forEach>
					<!-- 댓글기호 -->
					<c:if test="${dto.depth ne 0 }">ㄴ</c:if>
					
					<!-- 삭제되지 않은 글이라면 출력 가능 -->
					<c:if test="${dto.del_info ne -1}">
						<a href="view?idx=${dto.idx}&page=${param.page}">
							<font color="black">${dto.subject}</font>
						</a>
					</c:if>
					
					<!-- 삭제된 글은 클릭 불가 -->
					<c:if test="${dto.del_info eq -1}">
						<font color="gray">${dto.subject}</font>
					</c:if>
				</td>
				<td>${dto.name}</td>
				
				<c:if test="${dto.del_info ne -1}">
					<td>${fn:split(dto.regdate, ' ')[0]}</td>
				</c:if>

				<c:if test="${dto.del_info eq -1}">
					<td>unknown</td>	
				</c:if>	
				<td>${dto.readhit}</td>
			</tr>
		</c:forEach>
		<tr>
			<td colspan="5" align="center">
				${pageMenu}
			</td>
		</tr>
		<tr>
			<td colspan="5" align="right">
				<img src="resources/img/btn_reg.gif" onclick="location.href='insert_form?page=${param.page}'" style="cursor:pointer;">
			</td>
		</tr>
	</table>
</body>
</html>
  1. board_view.jsp
    • 게시글의 내용을 상세하게 볼 수 있으며, 댓글 작성, 글 삭제를 할 수 있다.
<%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8"%>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Insert title here</title>
	<script src="resources/js/HttpRequest.js"></script>
	<script>		
		function del() {
			let pwd = ${dto.pwd};
			let c_pwd = document.getElementById("c_pwd").value;
			
			if (!confirm("삭제하시겠습니까?")) {
				return;
			}
		
			if (c_pwd == '') {
				alert("비밀번호를 입력해주세요");
				return;
			}
			
			if (c_pwd != pwd) {
				alert("비밀번호가 일치하지 않습니다")
				return;
			}
			
			let url = "board_delete";
			let param = "idx=${dto.idx}";
			
			sendRequest(url, param, resultFn, "POST");
		}
			
		function resultFn() {
			if(xhr.readyState == 4 && xhr.status == 200) {
				let data = xhr.responseText;
				let json = (new Function('return' + data))();
				
				if(json[0].result == 'yes') {
					alert("게시물이 성공적으로 삭제되었습니다");
					location.href = "board_list?page=${param.page}";
				} else {
					alert("게시물 삭제에 실패했습니다");
				}
			}
		}
		
		function reply() {
			location.href = "reply_form?idx=${dto.idx}&page=${param.page}";
		}
	</script>
</head>
<body>
	<table border="1" align="center">	
		<caption>::게시글 상세보기</caption>
		<tr>
			<th>제목</th>
			<td>${dto.subject}</td>
		</tr>
		<tr>
			<th>작성자</th>
			<td>${dto.name}</td>
		</tr>
		<tr>
			<th>작성일</th>
			<td>${dto.regdate}</td>
		</tr>
		<tr>
			<th>ip</th>
			<td>${dto.ip}</td>
		</tr>
		<tr>
			<th>내용</th>
			<td width="500px" height="200px"><pre>${dto.content}</pre></td>
		</tr>
		<tr>
			<th>비밀번호</th>
			<td><input type="password" id="c_pwd"></td>
		</tr>
		<tr>
			<td colspan="2">
				<img src="resources/img/btn_list.gif" onclick="location.href='board_list?page=${param.page}'" style="cursor:pointer">
				
				<!-- 답글 -->
				<c:if test="${dto.depth lt 1}">
					<img src="resources/img/btn_reply.gif" onclick="reply()" style="cursor:pointer">
				</c:if>
				
			 	<img src="resources/img/btn_delete.gif" onclick="del()" style="cursor:pointer">
			</td>
		</tr>
	</table>
</body>
</html>
  1. insert_form.jsp
    • 새 글을 추가하는 페이지
<%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8"%>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Insert title here</title>
	<script>
		function send_check() {
			let f = document.f;
			
			f.submit();
		}
	</script>
</head>
<body>
	<form action="insert" name="f" method="POST">
		<input type="hidden" name="page" value="${param.page}">
		<table border="1" align="center">
			<caption>:::새 글 쓰기:::</caption>
			<tr>
				<th>제목</th>
				<td><input name="subject"></td>
			</tr>
			<tr>
				<th>작성자</th>
				<td><input name="name" value="${param.name}"></td>
			</tr>
			<tr>
				<th>내용</th>
				<td><textarea name="content" rows="10" cols="50" style="resize:none;"></textarea></td>
			</tr>
			<tr>
				<th>비밀번호</th>
				<td><input name="pwd" type="password"></td>
			</tr>
			<tr>
				<td colspan="2">
					<img src="resources/img/btn_reg.gif" onclick="send_check()" style="cursor:pointer;">
					<img src="resources/img/btn_back.gif" onclick="location.href='board_list?page=${param.page}'" style="cursor:pointer;">
				</td>
			</tr>
		</table>
	</form>
</body>
</html>
  1. reply_form.jsp
    • 댓글을 추가하는 페이지로, 새 글을 추가하는 insert_form.jsp와 형태가 비슷하다.
<%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8"%>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Insert title here</title>
	<script>
		function send_check() {
			let f = document.f;
			
			f.submit();
		}
	</script>
</head>
<body>
	<form action="reply" name="f" method="POST">
		<input type="hidden" name="page" value="${param.page}">
		<input type="hidden" name="idx" value="${param.idx}">
		<table border="1" align="center">
			<caption>:::댓글 쓰기:::</caption>
			<tr>
				<th>제목</th>
				<td><input name="subject"></td>
			</tr>
			<tr>
				<th>작성자</th>
				<td><input name="name"></td>
			</tr>
			<tr>
				<th>내용</th>
				<td><textarea name="content" rows="10" cols="50" style="resize:none;"></textarea></td>
			</tr>
			<tr>
				<th>비밀번호</th>
				<td><input name="pwd" type="password"></td>
			</tr>
			<tr>
				<td colspan="2">
					<img src="resources/img/btn_reg.gif" onclick="send_check()" style="cursor:pointer;">
					<img src="resources/img/btn_back.gif" onclick="location.href='board_list?page=${param.page}'" style="cursor:pointer;">
				</td>
			</tr>
		</table>
	</form>
</body>
</html>
  1. login_form.jsp
    • 로그인 페이지로, 정보를 입력하면 DB에서 해당 회원 정보가 존재하는지 확인하고, 존재하면 로그인을, 없으면 로그인 실패를 띄운다.
<%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8"%>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Insert title here</title>
	<script src="resources/js/HttpRequest.js"></script>
	<script>
		function send(f) {
			let id = f.id.value.trim();
			let pwd = f.pwd.value.trim();
			
			if(id == '') {
				alert('아이디를 입력해주세요');
				return;
			}
			if(pwd == '') {
				alert('비밀번호를 입력해주세요');
				return;
			}
			
			let url = "login";
			let param = "id="+id+"&pwd="+encodeURIComponent(pwd);
			
			sendRequest(url, param, myCheck, "POST");
		}
		
		function myCheck() {
			if(xhr.readyState == 4 && xhr.status == 200) {
				let data = xhr.responseText;
				let json = (new Function('return' + data))();
				
				if(json[0].param == "no_id") {
					alert("아이디가 존재하지 않습니다");
				} else if (json[0].param == "no_pwd") {
					alert("아이디와 비밀번호를 다시 확인해주세요")
				} else{
					alert("로그인 성공");
					location.href="board_list";
				}
			}
		}
	</script>
</head>
<body>
	<form>
		<table border="1" align="center">
			<caption>::로그인::</caption>
			<tr>
				<th>아이디</th>
				<td><input name="id"></td>
			</tr>
			<tr>
				<th>비밀번호</th>
				<td><input name="pwd" type="password"></td>
			</tr>
			<tr>
				<td colspan="2" align="center">
					<input type="button" value="로그인" onclick="send(this.form)">
				</td>
			</tr>
		</table>
	</form>
</body>
</html>
  1. member_insert_form.jsp
    • 회원가입 페이지로, 입력 받은 id가 DB에 존재하는지 확인하여 id 중복 조회를 수행하고 중복된 id가 없는 경우에 회원 가입을 처리한다.
    • 이메일 형식을 정규 표현식으로 체크했으며, 회원가입이 성공적으로 되었다면 Ajax를 통해 화면에 알림창을 띄우고 게시판 페이지로 이동한다.
    • 회원 가입을 실패했다면 현재 페이지에서 회원 가입을 다시 시도하라는 안내창을 띄운다.
<%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8"%>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Insert title here</title>
	<script src="resources/js/HttpRequest.js"></script>
	<script>
		let idCheck = false;
	
		// 아이디 중복체크
		function check_id() {
			let id = document.getElementById("id").value.trim();

			if (id == null) {
				alert("아이디를 입력하세요");
				return;
			}
			
			let url = "check_id";
			let param = "id="+id;
			
			sendRequest(url, param, resultFn, "POST");
		}
		
		function che() {
			idCheck = false;
		}
		
		function resultFn() {
			if(xhr.readyState == 4 && xhr.status == 200) {
				let data = xhr.responseText;
				let json = (new Function('return' + data))();
				
				if (json[0].res == "no") {
					alert("중복된 아이디입니다.");
					return;
				} else {
					alert("사용 가능한 아이디입니다.")
					idCheck = true;
				}
			}
		}
	
		// 회원가입
		function send(f) {
			let id = f.id.value.trim();
			let name = f.name.value.trim();
			let pwd = f.pwd.value.trim();
			let email = f.email.value.trim();
			
			// 유효성 검사
			if (id == '') {
				alert('아이디를 입력해주세요');
				return;
			}
			if (name == '') {
				alert('이름를 입력해주세요');
				return;
			}
			if (pwd == '') {
				alert('비밀번호를 입력해주세요');
				return;
			}
			if (email == '') {
				alert('이메일을 입력해주세요');
				return;
			}
			
			let regex = /[0-9a-zA-Z]([-_.]?[0-9a-zA-Z])*@[0-9a-zA-Z]([-_.]?[0-9a-zA-Z])*.[a-zA-Z]$/i;
			
			if (!regex.test(email)) {
				alert("이메일 형식이 일치하지 않습니다");
				return;
			}
			
			if (!idCheck) {
				alert("아이디 중복 체크가 필요합니다");
				return;
			}
			
			let url = "member_insert";
			let param = "id="+id+"&name="+name+"&pwd="+encodeURIComponent(pwd)+"&email="+email;
			
			sendRequest(url, param, registerFn, "POST");
		}
		
		function registerFn() {
			if(xhr.readyState == 4 && xhr.status == 200) {
				let data = xhr.responseText;
				let json = (new Function('return' + data))();
				
				if (json[0].regi == "yes") {
					alert("회원 가입이 완료되었습니다");
					location.href="board_list";
				} else {
					alert("회원 가입에 실패했습니다. 다시 시도해주세요");
					return;
				}
			}
		}
	</script>
</head>
<body>
	<form>
		<table border="1">
			<caption>:::회원가입:::</caption>
			 <tr>
			 	<th>아이디</th>
			 	<td>
			 		<input name="id" id="id" onchange="che()">
			 		<input type="button" value="중복체크" onclick="check_id()">
			 	</td>
			 </tr>
			 <tr>
			 	<th>이름</th>
			 	<td>
			 		<input name="name">
			 	</td>
			 </tr>
			 <tr>
			 	<th>비밀번호</th>
			 	<td>	
			 		<input name="pwd" type="password">
			 	</td>
			 </tr>
			 <tr>
			 	<th>이메일</th>
			 	<td>
			 		<input name="email">
			 	</td>
			 </tr>
			 <tr>
			 	<td colspan="2" align="center">
			 		<input type="button" value="가입" onclick="send(this.form)">
			 		<input type="button" value="취소" onclick="location.href='board_list'">
			 	</td>
			 </tr>
		</table>
	</form>
</body>
</html>

완성된 모습

  1. 게시판 페이지에서 게시글들의 정보와 버튼들을 확인할 수 있는데, 로그인을 안 한 상태라면 오른쪽에 로그인과 회원가입 버튼이 뜨도록 설정했다.

board spring 2.png

  1. 로그인을 안 한 상태여도 게시글을 누르면 상세하게 볼 수 있다. 해당 글의 idx 정보가 url에도 페이지 정보와 함께 표시되는 것을 확인할 수 있다.

board spring 3.png

  1. 로그인 버튼을 누르거나, 로그인을 안 한 상태에서 게시글 추가하기 및 답변 달기를 누르면 로그인 페이지로 넘어간다. 알맞는 회원 정보를 입력하면 로그인 성공이 뜨며 메인 게시판 페이지로 이동한다.

board spring 4.png
board spring 5.png

  1. 로그인 상태라면 오른쪽에 로그아웃 버튼만 보인다.

board spring 6.png

  1. 새 글을 작성할 때 작성자 이름에 현재 로그인한 계정의 이름을 넣었으며, 아직 input으로 넘기는 것 외에 다른 형태로 수정하지 않아 input 내의 value를 변경할 수 있는 상태다.
    • 모든 정보를 입력 해야만 등록하도록 설정되어 있다.

board spring 7.png

  1. 새 글을 추가하면 1페이지 가장 위에 업데이트 된다.

board spring 8.png

  1. 특정 게시글을 상세보기 한 후 답변 작성 버튼을 누르면 댓글을 작성한다.
    • 작성자 이름은 현재 로그인한 계정쪽에서 받아오도록 수정하지 않아 임의로 변경 가능한 상태다.
    • 모든 정보를 입력해야만 답변을 등록할 수 있다.
    • url에 원본 글의 idx와 페이지 정보도 함께 있는 것을 확인할 수 있다.

board spring 9.png

  1. 답변을 작성하면 답변 표시로 글이 추가된다.
    • 첫 테스트 때 step과 depth에 +1을 추가하는 것을 잊어서 수정하다 보니 원본 글의 조회수가 증가했다.

board spring 10.png

  1. 다른 페이지로 이동하면 url에도 해당 페이지 정보가 함께 반영된다.

board spring 11.png

  1. 로그아웃을 한 후 회원가입 버튼을 누르면 회원가입 페이지가 뜬다.
    • 중복 체크 시 이미 DB에 존재하는 id라면 중복된 아이디라는 알림이 뜬다.
      board spring 12.png